/*
 jChecs: a Java chess game sample 

 Copyright (C) 2006-2011 by David Cotton

 This program is free software; you can redistribute it and/or modify it under
 the terms of the GNU General Public License as published by the Free Software
 Foundation; either version 2 of the License, or (at your option) any later
 version.

 This program is distributed in the hope that it will be useful, but WITHOUT
 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

 You should have received a copy of the GNU General Public License along with
 this program; if not, write to the Free Software Foundation, Inc., 51 Franklin
 Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */
package fr.free.jchecs.core;

import java.io.BufferedReader;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.regex.Pattern;

import fr.free.jchecs.ai.EngineFactory;

import static fr.free.jchecs.core.BoardFactory.State.STARTING;
import static fr.free.jchecs.core.BoardFactory.Type.FASTEST;
import static fr.free.jchecs.core.Constants.APPLICATION_NAME;
import static fr.free.jchecs.core.Constants.APPLICATION_VERSION;
import static fr.free.jchecs.core.FENUtils.STANDART_STARTING_FEN;
import static fr.free.jchecs.core.FENUtils.toBoard;
import static fr.free.jchecs.core.SANUtils.toMove;

/**
 * Classes fournissant des fonctions utilitaires pour gérer la notation PGN.
 * <p>
 * Classe sûre vis-à-vis des threads.
 * </p>
 * 
 * @author David Cotton
 */
public final class PGNUtils {
	/** Modèle de découpage de chaines suivant les espaces. */
	private static final Pattern SPLITTER = Pattern.compile("[ ]+");

	/**
	 * Classe utilitaire : ne pas instancier.
	 */
	private PGNUtils() {
		// Rien de spécifique...
	}

	/**
	 * Formate une date suivant le standard PGN.
	 * 
	 * @param pDate
	 *            Date à formater.
	 * @return Chaine au format attendu par PGN.
	 */
	private static String formatPGNDate(final Date pDate) {
		assert pDate != null;

		return new SimpleDateFormat("yyyy.MM.dd").format(pDate);
	}

	/**
	 * Renvoi la description de partie correspondant à la première partie
	 * rencontrée dans un flux au format PGN.
	 * 
	 * @param pFlux
	 *            Flux contenant les données au format PGN.
	 * @return Description de partie correspondante.
	 * @throws PGNException
	 *             en cas d'erreur dans le format du flux PGN.
	 */
	public static Game toGame(final BufferedReader pFlux) throws PGNException {
		assert pFlux != null;

		final Game res = new Game();

		try {
			String ligneLue = pFlux.readLine();
			while (ligneLue != null) {
				String ligne = ligneLue.trim();
				if (ligne.startsWith("[")) {
					// Interprétation des tags d'en-tête...
					final int debTag = ligne.indexOf(" \"");
					if ((debTag >= 0) && ligne.endsWith("\"]")) {
						final String contenu = ligne.substring(debTag + 2,
								ligne.indexOf("\"]")).trim();
						if (ligne.startsWith("[Black \"")) {
							final Player joueur = res.getPlayer(false);
							if (contenu.startsWith(APPLICATION_NAME + '.')) {
								joueur.setEngine(EngineFactory
										.newInstance(contenu));
								joueur.setName(contenu
										.substring(APPLICATION_NAME.length() + 1));
							} else {
								joueur.setEngine(null);
								joueur.setName(contenu);
							}
						} else if (ligne.startsWith("[White \"")) {
							final Player joueur = res.getPlayer(true);
							if (contenu.startsWith(APPLICATION_NAME + '.')) {
								joueur.setEngine(EngineFactory
										.newInstance(contenu));
								joueur.setName(contenu
										.substring(APPLICATION_NAME.length() + 1));
							} else {
								joueur.setEngine(null);
								joueur.setName(contenu);
							}
						} else if (ligne.startsWith("[FEN \"")) {
							try {
								final Board depart = toBoard(contenu);
								res.resetTo(BoardFactory.valueOf(FASTEST,
										STARTING).derive(depart));
							} catch (final FENException e) {
								throw new PGNException("Invalid FEN tag", e);
							}
						}
					}
				} else {
					// Concaténation de la liste des mouvements...
					final StringBuilder sb = new StringBuilder();
					while (ligneLue != null) {
						ligne = ligneLue.trim();
						if (ligne.startsWith("[Event")) {
							break;
						}
						sb.append(' ').append(ligne);
						ligneLue = pFlux.readLine();
					}
					// Nettoyage de la chaine...
					int p = 0;
					int prof = 0;
					while (p < sb.length()) {
						final char c = sb.charAt(p);
						if ((c == '(') || (c == '{')) {
							// Supprime les commentaires, et les propositions de
							// nul...
							prof++;
						}
						if ((prof != 0) || (c == '+') || (c == '#')) {
							// Supprime les marqueurs d'échecs et de mat.
							sb.deleteCharAt(p);
						} else {
							if (c == 'O') {
								// Convertir les "o" majuscules en zéro...
								sb.setCharAt(p, '0');
							}
							p++;
						}
						if ((c == '}') || (c == ')')) {
							prof--;
						}
					}
					p = 0;
					while (p < sb.length()) {
						final char c = sb.charAt(p);
						if (c == '.') {
							// Supprime les numéros de coups...
							int deb = p - 1;
							while ((deb >= 0)
									&& Character.isDigit(sb.charAt(deb))) {
								deb--;
							}
							int fin = p + 1;
							while ((fin < sb.length())
									&& ((" .".indexOf(sb.charAt(fin))) >= 0)) {
								fin++;
							}
							sb.delete(deb + 1, fin);
						} else if (c == '$') {
							// Supprime les annotations numériques...
							int fin = p + 1;
							while ((fin < sb.length())
									&& Character.isDigit(sb.charAt(fin))) {
								fin++;
							}
							sb.delete(p, fin + 1);
						} else {
							p++;
						}
					}

					for (final String mvt : SPLITTER.split(sb.toString())) {
						if ("*".equals(mvt) || "1-0".equals(mvt)
								|| "0-1".equals(mvt) || "1/2-1/2".equals(mvt)) {
							break;
						}
						if (mvt.length() > 0) {
							try {
								res.moveFromCurrent(toMove(res.getBoard(),
										toNormalizedSAN(mvt)));
							} catch (final SANException e) {
								throw new PGNException("Invalid PGN stream", e);
							}
						}
					}

					break;
				}
				ligneLue = pFlux.readLine();
			}
		} catch (final IOException e) {
			throw new PGNException("PGN stream reading error", e);
		}

		return res;
	}

	/**
	 * Converti une chaîne de mouvement "PGN" en chaine de mouvement "SAN"
	 * normalisé.
	 * 
	 * @param pChaine
	 *            Chaine de mouvement PGN.
	 * @return Chaine de mouvement SAN.
	 */
	public static String toNormalizedSAN(final String pChaine) {
		assert pChaine != null;

		final StringBuilder res = new StringBuilder(pChaine);

		// Le marqueur 'P' pour les pions est possible avec PGN, pas avec SAN...
		if (res.charAt(0) == 'P') {
			res.deleteCharAt(0);
		}

		// Les promotions sont indiquées avec un '=' sous PGN, pas sous SAN...
		int p = res.indexOf("=");
		while (p >= 0) {
			res.deleteCharAt(p);
			p = res.indexOf("=");
		}

		// Les suffixes d'annotations '?' et '!' ne font pas partie de SAN...
		while ((res.length() > 0)
				&& ("?!".indexOf(res.charAt(res.length() - 1)) >= 0)) {
			res.deleteCharAt(res.length() - 1);
		}

		// Certains programmes ajoutent des '@' (totalement hors normes)...
		p = res.indexOf("@");
		while (p >= 0) {
			res.deleteCharAt(p);
			p = res.indexOf("@");
		}

		return res.toString();
	}

	/**
	 * Renvoi une chaîne correspondant à l'image de la partie au format PGN.
	 * 
	 * @param pPartie
	 *            Description de la partie.
	 * @return Chaine contenant la représentation de la partie au format PGN.
	 */
	public static String toPGN(final Game pPartie) {
		if (pPartie == null) {
			throw new NullPointerException("Missing game description");
		}

		final StringBuilder sb = new StringBuilder();

		sb.append("[Event \"" + APPLICATION_NAME + " v" + APPLICATION_VERSION
				+ " chess game\"]\n");
		String site = "?";
		try {
			final InetAddress lh = InetAddress.getLocalHost();
			site = lh.getHostName();
		} catch (final UnknownHostException e) {
			// Pas grave, on peut se passer de cette information...
		}
		sb.append("[Site \"").append(site).append("\"]\n");
		sb.append("[Date \"").append(formatPGNDate(new Date())).append("\"]\n");
		sb.append("[Round \"-\"]\n");
		Player joueur = pPartie.getPlayer(true);
		String nom = joueur.getName();
		if (joueur.getEngine() != null) {
			nom = APPLICATION_NAME + '.' + nom;
		}
		sb.append("[White \"").append(nom).append("\"]\n");
		joueur = pPartie.getPlayer(false);
		nom = joueur.getName();
		if (joueur.getEngine() != null) {
			nom = APPLICATION_NAME + '.' + nom;
		}
		sb.append("[Black \"").append(nom).append("\"]\n");
		final String resultat;
		switch (pPartie.getState()) {
		case BLACK_MATES:
			resultat = "0-1";
			break;
		case DRAWN_BY_50_MOVE_RULE:
		case DRAWN_BY_TRIPLE_REPETITION:
		case STALEMATE:
			resultat = "1/2-1/2";
			break;
		case WHITE_MATES:
			resultat = "1-0";
			break;
		case IN_PROGRESS:
			resultat = "*";
			break;
		default:
			resultat = "*";
			assert false;
		}
		sb.append("[Result \"").append(resultat).append("\"]\n");
		final String depart = pPartie.getStartingPosition();
		if (!depart.equals(STANDART_STARTING_FEN)) {
			sb.append("[SetUp \"1\"]\n");
			sb.append("[FEN \"" + depart + "\"]\n");
		}
		sb.append('\n');
		int col = 0;
		for (final String san : pPartie.getSANStrings()) {
			// PGN attend des "o" majuscules plutôts que les zéros du SAN
			// standard...
			String pgn = san.replace("0-0-0", "O-O-O");
			pgn = pgn.replace("0-0", "O-O");
			// ... et des "#" plutôt que "++" pour le mat...
			pgn = pgn.replace("++", "#");
			final int l = pgn.length();
			col += l;
			if (col >= 80) {
				sb.append('\n');
				col = l;
			}
			sb.append(pgn);
		}
		col += resultat.length();
		if (col >= 80) {
			sb.append('\n');
		}
		sb.append(resultat).append('\n');

		return sb.toString();
	}

}
